package com.copenhagensoftware.g2econ;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Logger;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import soap.economic.com.AccountHandle;
import soap.economic.com.ArrayOfBudgetFigureData;
import soap.economic.com.ArrayOfBudgetFigureHandle;
import soap.economic.com.BudgetFigureData;
import soap.economic.com.Connector;
import soap.economic.com.SoapConnection;

import com.sforce.ws.SoapFaultException;

/**
 * @author Copenhagen Software Aps
 * Creates or updates e-conomic budget figure for a request containing:
 * - economicAgreementNumber (integer)
 * - economicUserName (string)
 * - economicPassword (string)
 * - rowNumber (integer)
 * - accountNumber (integer)
 * - text (string)
 * - fromDate (date: yyyy-mm-dd or dd/mm/yyyy)
 * - toDate (date: yyyy-mm-dd or dd/mm/yyyy)
 * - amount (decimal: dddd.dd)
 */
@SuppressWarnings("serial")
public class CreateOrUpdateBudgetFigureServlet extends HttpServlet {
	
	private static final Logger log = Logger.getLogger(CreateOrUpdateBudgetFigureServlet.class.getName());
	
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		try {
			SoapConnection sc = Connector.newConnection(null, null);
			
			// Extract e-conomic agreement number from the request.
			String agreementNumberString = req.getParameter("economicAgreementNumber");
			if (isNullOrEmpty(agreementNumberString)) {
				returnBadRequest(resp, "Missing e-conomic agreement number.");
				return;
			}
			int agreementNumber;
			try {
				agreementNumber = Integer.parseInt(agreementNumberString);
			} catch (NumberFormatException nfe) {
				returnBadRequest(resp, String.format("Wrong format for e-conomic agreement number (integer): '%s'.", agreementNumberString));
				return;
			}
			
			// Extract e-conomic user name from the request.
			String userName = req.getParameter("economicUserName");
			if (isNullOrEmpty(userName)) {
				returnBadRequest(resp, "Missing e-conomic user name.");
				return;
			}
			
			// Extract e-conomic password from the request.
			String password = req.getParameter("economicPassword");
			if (isNullOrEmpty(password)) {
				returnBadRequest(resp, "Missing e-conomic password.");
				return;
			}

			// Maintain session with e-conomic server.
			try {
				String sessionId = sc.Connect(agreementNumber, userName, password);
				sc.getConfig().setSessionId(sessionId);
			} catch (SoapFaultException e) {
				if (e.getMessage() != null && e.getMessage().startsWith("Economic.Api.Exceptions.AuthenticationException(E02000): Access denied.")) {
					returnBadRequest(resp, "Unable to connect to e-conomic.");
					return;
				}
				throw e;
			}

			// Extract budget figure information from the request.
			BudgetFigureInfo info = null;
			try {
				info = extractBudgetFigure(req);
			} catch (ValidationException e) {
				returnBadRequest(resp, String.format("Error: %s.", e.getReason()));
				return;
			}
			
			AccountHandle accountHandle = sc.Account_FindByNumber(info.getAccountNumber());
			if(accountHandle == null){
				returnBadRequest(resp, String.format("Unknown account number '%d' in row %d.", info.getAccountNumber(), info.getRowNumber()));
				return;
			}
			
			// Inform caller, that data is valid
			resp.getWriter().print(String.format("Row %d sucessfully validated. ", info.getRowNumber()));
			
	
			// Update or create budget figure.
			//
			// Get existing budget figures (all budget figures that overlap the period are returned).
			// If a budget figure with same account number, to and from date exists we update the budget figure
			// else a new budget figure is created.
			Boolean updated = false;
			ArrayOfBudgetFigureHandle budgetFiguresWrapper = sc.BudgetFigure_FindByDate(info.getFromDate(), info.getToDate());
			if (budgetFiguresWrapper.getBudgetFigureHandle().length > 0) {
				ArrayOfBudgetFigureData budgetFigureDatasWrapper = sc.BudgetFigure_GetDataArray(budgetFiguresWrapper);
				BudgetFigureData[] budgetFiguresData = budgetFigureDatasWrapper.getBudgetFigureData();

				// Gather budget figures to update.
				for (BudgetFigureData budgetFigureData : budgetFiguresData) {
					if (info.getFromDate().equals(budgetFigureData.getFromDate()) && info.getToDate().equals(budgetFigureData.getToDate())
							&& accountHandle.getNumber() == budgetFigureData.getAccountHandle().getNumber()) {
						budgetFigureData.setText(info.getText());
						budgetFigureData.setAmountDefaultCurrency(info.getAmount());
						sc.BudgetFigure_UpdateFromData(budgetFigureData);
						updated = true;
					}
				}
				// Else create new budget figure.
				if(updated == false) {
					sc.BudgetFigure_Create(accountHandle, info.getText(), info.getFromDate(), info.getToDate(), info.getAmount());
				}
		
			}
			// No existing budget figures, so create a new one.
			else {
				sc.BudgetFigure_Create(accountHandle, info.getText(), info.getFromDate(), info.getToDate(), info.getAmount());
			}
		
			// Return status.
			resp.getWriter().print(String.format(" Budget figure was: %s.", (updated ? "updated" : "created")  ));
			
			// Disconnect.
			sc.Disconnect();

			// Return success.
			resp.setStatus(201);

		} catch (Throwable e) {
			resp.setStatus(400); //Internal server error (when using HTTP status code 500 in our response google fails to piggy-back our answer.)
			resp.getWriter().print(String.format("Error: '%s'.", e.getMessage()));
			
			ByteArrayOutputStream temp = new ByteArrayOutputStream();
			e.printStackTrace(new PrintStream(temp, true));
			log.severe(temp.toString());
			return;
		}
	}

	/**
	 * Extracts information for a budget figure from a request.
	 * @param req The request.
	 * @return Budget figure information or null
	 * @throws ValidationException When the information in the request is not valid (the exception contains a reason).
	 */
	public BudgetFigureInfo extractBudgetFigure(HttpServletRequest req) throws ValidationException {
		String rowNumberString = req.getParameter("rowNumber");
		if (isNullOrEmpty(rowNumberString))
			throw new ValidationException("Missing row number");

		int rowNumber;
		try {
			rowNumber = Integer.parseInt(rowNumberString);
		} catch (NumberFormatException nfe) {
			throw new ValidationException(String.format("Wrong format for row number (integer): '%s'", rowNumberString));
		}
		
		String accountNumberString = req.getParameter("accountNumber");
		if (isNullOrEmpty(accountNumberString))
			throw new ValidationException(String.format("Missing account number"));

		int accountNumber;
		try {
			accountNumber = Integer.parseInt(accountNumberString);
		} catch (NumberFormatException nfe) {
			throw new ValidationException(String.format("Wrong format for account number (integer): '%s' in row '%d'" , accountNumberString, rowNumber));
		}

		String text = req.getParameter("text");
		if (isNullOrEmpty(text))
			throw new ValidationException(String.format("Missing text in row '%d'", rowNumber));

		String fromDateString = req.getParameter("fromDate");
		if (isNullOrEmpty(fromDateString))
			throw new ValidationException(String.format("Missing from date in row '%d'", rowNumber));

		Calendar fromCal;
		try {
			Date date = parseDate(fromDateString);
			fromCal = Calendar.getInstance();
			fromCal.setTime(date);
		} catch(ParseException pe) {
			throw new ValidationException(String.format("Wrong format for from date (yyyy-mm-dd or dd/mm/yyyy): '%s' in row '%d'", fromDateString, rowNumber));
		}

		String toDateString = req.getParameter("toDate");
		if (isNullOrEmpty(toDateString))
			throw new ValidationException(String.format("Missing to date in row '%d'", rowNumber));

		Calendar toCal;
		try {
			Date toDate = parseDate(toDateString);
			toCal = Calendar.getInstance();
			toCal.setTime(toDate);
		} catch (ParseException e) {
			throw new ValidationException(String.format("Wrong format for to date (yyyy-mm-dd or dd/mm/yyyy): '%s' in row '%d'", toDateString, rowNumber));
		}

		if (toCal.before(fromCal)) {
			throw new ValidationException(String.format("To date cannot be before from date: '%s' < '%s' in row '%d'", toDateString, fromDateString, rowNumber));
		}

		String amountString = req.getParameter("amount");
		if (isNullOrEmpty(amountString))
			throw new ValidationException(String.format("Missing amount in row '%d'", rowNumber));

		double amount;
		try {
			amount = Double.parseDouble(amountString);
		} catch (NumberFormatException e) {
			throw new ValidationException(String.format("Wrong format for amount (dddd.dd): '%s' in row '%d'", amountString, rowNumber));
		}
		
		return new BudgetFigureInfo(rowNumber,accountNumber, text, fromCal, toCal, amount);
	}

	private Date parseDate(String dateString) throws ParseException {
		Date date = null;
		try{
			DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
			date = (Date)formatter.parse(dateString);
		} catch(ParseException pe){
			DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
			date = (Date)formatter.parse(dateString);
		}
		return date;
	}

	private boolean isNullOrEmpty(String str) {
		return str == null || str == "";
	}

	/**
	 * Sets the HTTP return code of the response to 400 and writes the cause.
	 * @param resp The response.
	 * @param cause The cause.
	 * @throws IOException When a writing problem occurs.
	 */
	private void returnBadRequest(HttpServletResponse resp, String cause) throws IOException {
		resp.setStatus(400);
		resp.getWriter().println(cause);
	}

	/**
	 * ValidationExceptions are thrown if the request contains invalid information for a budget figure.
	 * The exception contains a human readable string with the reason for the validation exception.
	 */
	private class ValidationException extends Exception {
		private String reason;

		public ValidationException(String reason) {
			this.reason = reason;
		}

		public String getReason() {
			return reason;
		}
	}

	/**
	 * Represents budget figure information from a request. 
	 */
	private class BudgetFigureInfo {
		private int rowNumber;
		private int accountNumber;
		private String text;
		private Calendar fromDate;
		private Calendar toDate;
		private double amount;

		public BudgetFigureInfo(int rowNumber, int accountNumber, String text, Calendar fromDate, Calendar toDate, double amount) {
			this.rowNumber = rowNumber;
			this.accountNumber = accountNumber;
			this.text = text;
			this.fromDate = fromDate;
			this.toDate = toDate;
			this.amount = amount;
		}

		public int getRowNumber() {
			return rowNumber;
		}
		
		public int getAccountNumber() {
			return accountNumber;
		}

		public String getText() {
			return text;
		}

		public Calendar getFromDate() {
			return fromDate;
		}

		public Calendar getToDate() {
			return toDate;
		}

		public double getAmount() {
			return amount;
		}
	}

}
